<!--
@license
Copyright (c) 2014 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
Code distributed by Google as part of the polymer project is also
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
-->

<link rel="import" href="settings.html">
<link rel="import" href="css-parse.html">

<script>

  Polymer.StyleUtil = (function() {
    var settings = Polymer.Settings;

    return {
      // chrome 49 has semi-working css vars, check if box-shadow works
      // safari 9.1 has a recalc bug: https://bugs.webkit.org/show_bug.cgi?id=155782
      NATIVE_VARIABLES: Polymer.Settings.useNativeCSSProperties,
      MODULE_STYLES_SELECTOR: 'style, link[rel=import][type~=css], template',
      INCLUDE_ATTR: 'include',

      toCssText: function(rules, callback) {
        if (typeof rules === 'string') {
          rules = this.parser.parse(rules);
        }
        if (callback) {
          this.forEachRule(rules, callback);
        }
        return this.parser.stringify(rules, this.NATIVE_VARIABLES);
      },

      forRulesInStyles: function(styles, styleRuleCallback, keyframesRuleCallback) {
        if (styles) {
          for (var i=0, l=styles.length, s; (i<l) && (s=styles[i]); i++) {
            this.forEachRuleInStyle(
                s,
                styleRuleCallback,
                keyframesRuleCallback);
          }
        }
      },

      forActiveRulesInStyles: function(styles, styleRuleCallback, keyframesRuleCallback) {
        if (styles) {
          for (var i=0, l=styles.length, s; (i<l) && (s=styles[i]); i++) {
            this.forEachRuleInStyle(
                s,
                styleRuleCallback,
                keyframesRuleCallback,
                true
              );
          }
        }
      },

      rulesForStyle: function(style) {
        if (!style.__cssRules && style.textContent) {
          style.__cssRules = this.parser.parse(style.textContent);
        }
        return style.__cssRules;
      },

      // Tests if a rule is a keyframes selector, which looks almost exactly
      // like a normal selector but is not (it has nothing to do with scoping
      // for example).
      isKeyframesSelector: function(rule) {
        return rule.parent &&
            rule.parent.type === this.ruleTypes.KEYFRAMES_RULE;
      },

      forEachRuleInStyle: function(style, styleRuleCallback, keyframesRuleCallback, onlyActiveRules) {
        var rules = this.rulesForStyle(style);
        var styleCallback, keyframeCallback;
        if (styleRuleCallback) {
          styleCallback = function(rule) {
            styleRuleCallback(rule, style);
          };
        }
        if (keyframesRuleCallback) {
          keyframeCallback = function(rule) {
            keyframesRuleCallback(rule, style);
          }
        }
        this.forEachRule(
          rules,
          styleCallback,
          keyframeCallback,
          onlyActiveRules
        );
      },

      forEachRule: function(node, styleRuleCallback, keyframesRuleCallback, onlyActiveRules) {
        if (!node) {
          return;
        }
        var skipRules = false;
        if (onlyActiveRules) {
          if (node.type === this.ruleTypes.MEDIA_RULE) {
            var matchMedia = node.selector.match(this.rx.MEDIA_MATCH);
            if (matchMedia) {
              // if rule is a non matching @media rule, skip subrules
              if (!window.matchMedia(matchMedia[1]).matches) {
                skipRules = true;
              }
            }
          }
        }
        if (node.type === this.ruleTypes.STYLE_RULE) {
          styleRuleCallback(node);
        } else if (keyframesRuleCallback &&
                   node.type === this.ruleTypes.KEYFRAMES_RULE) {
          keyframesRuleCallback(node);
        } else if (node.type === this.ruleTypes.MIXIN_RULE) {
          skipRules = true;
        }
        var r$ = node.rules;
        if (r$ && !skipRules) {
          for (var i=0, l=r$.length, r; (i<l) && (r=r$[i]); i++) {
            this.forEachRule(r, styleRuleCallback, keyframesRuleCallback, onlyActiveRules);
          }
        }
      },

      // add a string of cssText to the document.
      applyCss: function(cssText, moniker, target, contextNode) {
        var style = this.createScopeStyle(cssText, moniker);
        return this.applyStyle(style, target, contextNode);
      },

      applyStyle: function(style, target, contextNode) {
        target = target || document.head;
        var after = (contextNode && contextNode.nextSibling) ||
          target.firstChild;
        this.__lastHeadApplyNode = style;
        return target.insertBefore(style, after);
      },

      createScopeStyle: function(cssText, moniker) {
        var style = document.createElement('style');
        if (moniker) {
          style.setAttribute('scope', moniker);
        }
        style.textContent = cssText;
        return style;
      },

      __lastHeadApplyNode: null,

      // insert a comment node as a styling position placeholder.
      applyStylePlaceHolder: function(moniker) {
        var placeHolder = document.createComment(' Shady DOM styles for ' +
          moniker + ' ');
        var after = this.__lastHeadApplyNode ?
          this.__lastHeadApplyNode.nextSibling : null;
        var scope = document.head;
        scope.insertBefore(placeHolder, after || scope.firstChild);
        this.__lastHeadApplyNode = placeHolder;
        return placeHolder;
      },

      cssFromModules: function(moduleIds, warnIfNotFound) {
        var modules = moduleIds.trim().split(' ');
        var cssText = '';
        for (var i=0; i < modules.length; i++) {
          cssText += this.cssFromModule(modules[i], warnIfNotFound);
        }
        return cssText;
      },

      // returns cssText of styles in a given module; also un-applies any
      // styles that apply to the document.
      cssFromModule: function(moduleId, warnIfNotFound) {
        var m = Polymer.DomModule.import(moduleId);
        if (m && !m._cssText) {
          m._cssText = this.cssFromElement(m);
        }
        if (!m && warnIfNotFound) {
          console.warn('Could not find style data in module named', moduleId);
        }
        return m && m._cssText || '';
      },

      // support lots of ways to discover css...
      cssFromElement: function(element) {
        var cssText = '';
        // if element is a template, get content from its .content
        var content = element.content || element;
        var e$ = Polymer.TreeApi.arrayCopy(
          content.querySelectorAll(this.MODULE_STYLES_SELECTOR));
        for (var i=0, e; i < e$.length; i++) {
          e = e$[i];
          // look inside templates for elements
          if (e.localName === 'template') {
            // retain css content when specified,
            if (!e.hasAttribute('preserve-content')) {
              cssText += this.cssFromElement(e);
            }
          } else {
            // style elements inside dom-modules will apply to the main document
            // we don't want this, so we remove them here.
            if (e.localName === 'style') {
              var include = e.getAttribute(this.INCLUDE_ATTR);
              // now support module refs on 'styling' elements
              if (include) {
                cssText += this.cssFromModules(include, true);
              }
              // get style element applied to main doc via HTMLImports polyfill
              e = e.__appliedElement || e;
              e.parentNode.removeChild(e);
              cssText += this.resolveCss(e.textContent, element.ownerDocument);
            // it's an import, assume this is a text file of css content.
            // TODO(sorvell): plan is to deprecate this way to get styles;
            // remember to add deprecation warning when this is done.
            } else if (e.import && e.import.body) {
              cssText += this.resolveCss(e.import.body.textContent, e.import);
            }
          }
        }
        return cssText;
      },

      isTargetedBuild: function(buildType) {
        return settings.useNativeShadow ? buildType === 'shadow' : buildType === 'shady';
      },

      cssBuildTypeForModule: function (module) {
        var dm = Polymer.DomModule.import(module);
        if (dm) {
          return this.getCssBuildType(dm);
        }
      },

      getCssBuildType: function(element) {
        return element.getAttribute('css-build');
      },

      // Walk from text[start] matching parens
      // returns position of the outer end paren
      _findMatchingParen: function(text, start) {
        var level = 0;
        for (var i=start, l=text.length; i < l; i++) {
          switch (text[i]) {
            case '(':
              level++;
              break;
            case ')':
              if (--level === 0) {
                return i;
              }
              break;
          }
        }
        return -1;
      },

      processVariableAndFallback: function(str, callback) {
        // find 'var('
        var start = str.indexOf('var(');
        if (start === -1) {
          // no var?, everything is prefix
          return callback(str, '', '', '');
        }
        //${prefix}var(${inner})${suffix}
        var end = this._findMatchingParen(str, start + 3);
        var inner = str.substring(start + 4, end);
        var prefix = str.substring(0, start);
        // suffix may have other variables
        var suffix = this.processVariableAndFallback(str.substring(end + 1), callback);
        var comma = inner.indexOf(',');
        // value and fallback args should be trimmed to match in property lookup
        if (comma === -1) {
          // variable, no fallback
          return callback(prefix, inner.trim(), '', suffix);
        }
        // var(${value},${fallback})
        var value = inner.substring(0, comma).trim();
        var fallback = inner.substring(comma + 1).trim();
        return callback(prefix, value, fallback, suffix);
      },

      rx: {
        VAR_ASSIGN: /(?:^|[;\s{]\s*)(--[\w-]*?)\s*:\s*(?:([^;{]*)|{([^}]*)})(?:(?=[;\s}])|$)/gi,
        MIXIN_MATCH: /(?:^|\W+)@apply\s*\(?([^);\n]*)\)?/gi,
        VAR_CONSUMED: /(--[\w-]+)\s*([:,;)]|$)/gi,
        ANIMATION_MATCH: /(animation\s*:)|(animation-name\s*:)/,
        MEDIA_MATCH: /@media[^(]*(\([^)]*\))/,
        IS_VAR: /^--/,
        BRACKETED: /\{[^}]*\}/g,
        HOST_PREFIX: '(?:^|[^.#[:])',
        HOST_SUFFIX: '($|[.:[\\s>+~])'
      },

      resolveCss: Polymer.ResolveUrl.resolveCss,
      parser: Polymer.CssParse,
      ruleTypes: Polymer.CssParse.types

    };

  })();

</script>
